package com.laamella.syntherface.synths.midi;

import com.laamella.syntherface.synths.models.ParameterModel;
import com.laamella.syntherface.synths.models.StringModel;

/**
 * Used by {@link BinaryData} to check, encode or decode a list of data elements
 * to/from binary data.
 * 
 * <p>
 * There is a major difference between parameter field classes, and static field
 * classes. Parameter field classes take a value from a parameter and encode it
 * into the byte[], or take a value from the byte[] and store it in the
 * parameter. Static field classes contain a static value that does not change,
 * and on encode will store that value in the byte[], and on decode will check
 * the value in the byte[] with the static value, throwing an exception if they
 * don't match.
 * 
 * <p>
 * Contains many inner classes that implement {@link BinaryField}.
 * 
 * @author danny
 */
public abstract class BinaryField {

	protected abstract void encode(byte[] data) throws Exception;

	protected abstract void decode(byte[] data) throws Exception;

	protected static final String DecodeErrorStaticDataMismatch = "Decode error, static data mismatch.";

	public static class StaticUnsignedInt7Field extends BinaryFieldWithOffset {

		private final int value;

		public StaticUnsignedInt7Field(int offset, int value) {
			super(offset);
			this.value = value;
		}

		@Override protected void decode(byte[] data) throws Exception {
			if ((data[offset] & 0x7F) != value) {
				throw new Exception(DecodeErrorStaticDataMismatch);
			}
		}

		@Override protected void encode(byte[] data) throws Exception {
			data[offset] = (byte) (value & 0x7F);
		}

	}

	public static class UnsignedInt7Field extends BinaryFieldWithOffsetAndParameterModel {

		public UnsignedInt7Field(int offset, ParameterModel parameterModel) {
			super(offset, parameterModel);
		}

		@Override protected void decode(byte[] data) throws Exception {
			setValue((byte) data[offset] & 0x7F);
		}

		@Override protected void encode(byte[] data) throws Exception {
			// TODO Auto-generated method stub
			data[offset] = (byte) (getValue() & 0x7F);
		}

	}

	/**
	 * Utility class.
	 * 
	 * @author danny
	 * 
	 */
	public static abstract class BinaryFieldWithOffset extends BinaryField {
		protected final int offset;

		public BinaryFieldWithOffset(int offset) {
			this.offset = offset;
		}
	}

	/**
	 * Utility class.
	 */
	public static abstract class BinaryFieldWithOffsetAndParameterModel extends BinaryFieldWithOffset {
		private final ParameterModel parameterModel;

		public BinaryFieldWithOffsetAndParameterModel(int offset, ParameterModel parameterModel) {
			super(offset);
			this.parameterModel = parameterModel;
		}

		protected void setValue(int newValue) {
			newValue = modifyBeforeSetValue(newValue);
			System.out.println("Setting " + parameterModel.toString() + " to " + newValue);
			parameterModel.setValue(newValue);
		}

		/**
		 * Override if the binary field and the parameter value don't
		 * map 1:1
		 * 
		 * @param newValue
		 * @return
		 */
		protected int modifyBeforeSetValue(int newValue) {
			return newValue;
		}

		protected int getValue() {
			return modifyAfterGetValue(parameterModel.getValue());
		}

		/**
		 * Override if the binary field and the parameter value don't
		 * map 1:1
		 * 
		 * @param value
		 * @return
		 */
		protected int modifyAfterGetValue(int value) {
			return value;
		}
	}

	public static class StaticBytesField extends BinaryFieldWithOffset {
		private final byte[] staticData;

		public StaticBytesField(int offset, byte[] staticData) {
			super(offset);
			this.staticData = staticData;
		}

		@Override protected void decode(byte[] data) throws Exception {
			for (int i = 0; i < staticData.length; i++) {
				if (data[i + offset] != staticData[i]) {
					throw new Exception(DecodeErrorStaticDataMismatch);
				}
			}
		}

		@Override public void encode(byte[] data) {
			for (int i = 0; i < staticData.length; i++) {
				data[i + offset] = staticData[i];
			}
		}
	}

	public static class StaticTextField extends BinaryFieldWithOffset {

		private final String staticText;

		public StaticTextField(int offset, String staticText) {
			super(offset);
			this.staticText = staticText;
		}

		@Override protected void decode(byte[] data) throws Exception {
			for (int i = 0; i < staticText.length(); i++) {
				char c = (char) data[i + offset];
				if (c != '\0') {
					if (c != staticText.charAt(i)) {
						throw new Exception("Decode error, static text (" + staticText + ") mismatch.");
					}
				}
			}
		}

		@Override public void encode(byte[] data) {
			for (int i = 0; i < staticText.length(); i++) {
				data[i + offset] = (byte) staticText.charAt(i);
			}
		}

	}

	public static class TextField extends BinaryFieldWithOffset {
		private final StringModel textModel;
		private final int length;

		public TextField(int offset, StringModel textModel, int length) {
			super(offset);
			this.textModel = textModel;
			this.length = length;
		}

		@Override protected void decode(byte[] data) throws Exception {
			String result = "";
			for (int i = 0; i < length; i++) {
				char c = (char) data[i + offset];
				if (c != '\0') {
					result += c;
				}
			}
			textModel.setText(result);
		}

		@Override public void encode(byte[] data) {
			String encodeMe = textModel.getText();
			for (int i = 0; i < length; i++) {
				if (i < encodeMe.length()) {
					data[i + offset] = (byte) encodeMe.charAt(i);
				} else {
					data[i + offset] = 0;
				}
			}
		}
	}

	/**
	 * A few bits within a byte, mapped to a parameterModel.
	 * 
	 * @author danny
	 * 
	 */
	public static class BitField extends BinaryFieldWithOffsetAndParameterModel {

		private final int startBit;
		private final int bitCount;

		/**
		 * {@inheritDoc}
		 * 
		 * @param startBit
		 *                bitNumber to start at. 0..7. Another
		 *                interpretation: the number of bits to shift
		 *                right before decoding.
		 * @param bitCount
		 *                amount of bits in the field. The rest is
		 *                masked away.
		 */
		public BitField(int offset, ParameterModel parameterModel, int startBit, int bitCount) {
			super(offset, parameterModel);
			this.startBit = startBit;
			this.bitCount = bitCount;
		}

		@Override protected void decode(byte[] data) throws Exception {
			setValue(Bits.getBitField(data[offset], startBit, bitCount));
		}

		@Override protected void encode(byte[] data) throws Exception {
			data[offset] = Bits.setBitField(data[offset], startBit, bitCount, getValue());
		}

	}
}
